這篇文章會用不一樣的觀點來介紹Ruby and Rails,寫過 Javascript 的讀者們,相信這篇文章對你們會比較有感覺。
Ruby 為正統的OOP語言,但Ruby也 開了一扇functional programming的窗,讓我們可以使用,而今天我就要為讀者們打開這扇窗戶。
首先要介紹什麼是區塊block?block 為以 do ... end 或者以 {} 包裹住的區塊,以下面的例子來說,{|_| _ + 1} 就是 block
[1, 2, 3].map {|_| _ + 1}
在 Ruby 什麼東西都是物件,不過也有例外,例如Block本身就不是物件。Block沒有辦法單獨的存在,也沒辦法指定給某個變數。雖然是例外,但 block卻是Ruby常用的技巧。不僅面試常考,很多底層也很常使用block
{|_| _ + 1} #=> 不行這樣表示
a = {|_| _ + 1} #=> 當作變數也不行
#==== 取而代之,我們可以這樣寫
a = -> (v) { v + 1 }
下面的寫法為 Proc。
a = -> (v) { v + 1 }
Block 沒有辦法單獨存在,但我們可以使用 Proc 類別,把 Block 物件化。Proc全名稱為 Proceduce,而Proc一共有兩種,一種為lambda, 而另外一種為 Proc
#======= lambda
-> { 'foo' }
#=> #<Proc:0x00007f883d48dfd8@(irb):2 (lambda)>
lambda { 'foo' }
#=> #<Proc:0x00007f883d49f0f8@(irb):3 (lambda)>
#======= Proc
Proc.new { 'foo' }
#=> #<Proc:0x00007f883d4afc50@(irb):4>
lambda { 'foo' } #=> Proc
-> { 'foo' }.class #=> Proc
Proc.new { 'foo' } #=> Proc
接著,講講如何執行procedure
adder = -> (x) {x+1}
# 4種 notation
adder.call(1)
adder.(1)
adder.=== 1
adder[1]
一般來說,除了call 這種 notation 以外,其他用法長相太奇異,一般來說都不建議使用,但如果想要用proc,又想要在code review通關的話,建議使用最後一種notation,因為可以被當作使用陣列矇混過去。
def arb_mtd
# 程式碼邏輯(多行)
adder = -> (x) {x+1}
# 程式碼邏輯(多行)
adder[1]
# 程式碼邏輯(多行)
end
Ruby on Rails是很物件導向的語言,而漢漢老師曾經有一段時間因前端部門的人太少,我被轉調到前端部門。聽到大家討論在前端專案實作什麼寫法的時候,第一次聽到functional programming這個名詞。起初對於什麼是functional programming一頭霧水,看到同事的程式碼嚇到,原來functional programming在專案上竟然能夠呈現得那麼好看!
主要也是同事也好看。
總之開始看下列例子。以下的例子為將[1, 2, 3] 的陣列分別加1,回傳新的值
[1, 2, 3].map {|_| _ + 1}
我們使用Javascript來詮釋以上的寫法
/* es6 map */
[1, 2, 3].map(e => e + 1)
/* map in lodash */
map([1, 2, 3], e => e + 1)
依樣畫葫蘆,我們也將原本Ruby的寫法進行改寫。這裏先不要理會&的寫法,我們只是盡量將JS, Ruby寫法達到一致。
[1, 2, 3].map(&->(x) { x + 1 })
Ruby, JS兩個語言使用了匿名函式如下
e => e + 1 # anonymous in js
->(x) { x + 1 } # anonymous in ruby
並且,這兩個匿名函式皆為 Pure function,又為 First Class Citizens。今天突然講了很多新名詞,不過我會一一介紹給大家。
我們先介紹Pure function,Pure Function 有一個最重要的特性是當輸入了相同的值,吐出的值都是相等的結果,也有人說這種特性是可快取的(cached)。Vue2 的 computed property 即為一種Pure function,使用Pure function 不會有side effect,意即不會改變任何結果!
什麼是First Class Citizens?First Class Citizens 為將function 視為參數,指派到其他function使用。以上例來說,->(x) { x + 1 }為第一等公民,它將自己提供給別人給使用。
使用其他function作為參數使用的function,或者回傳結果為function都為 Higher Order Function。上述的map 使用了匿名函數,因此map高階函數 higher order function。
我們舉個簡單的例子講解 Higher Order Function 和 First Class Citizens 之間的關係
function ask(question, yes, no) {
if (confirm(question)) yes()
else no();
}
function showOk() {
alert( "OK" );
}
function showCancel() {
alert( "Canceled" );
}
// usage: functions showOk, showCancel are passed as arguments to ask
ask("Do you agree?", showOk, showCancel);
上述例子我們可以知道 showOk, showCancel都被ask拿來取用,因此上述例子的 Higher Order Function 為 ask,而 showOk, showCancel 則為 First Class Citizens 。
以下列出 Javascript 常見的高階函數,其中hello, square 是First Class Citizens,而使用hello, square 的setTimeout, map則為 higher order function。
// setTimeout
const hello = () => console.log("Hello!")
window.setTimeout((hello), 1000)
// map (用lodash的觀點來看)
const square = (n) => (n * n);
_.map([4, 8], square);
// => [16, 64]
在 Rails不難發現Higher Order Function 的蹤跡,以下舉一些複雜的例子。
# gem: AASM
#=== 定義訂單完成動作
event(:complete, success: -> { update_status_time!(:done_at) }) do
transitions from: [Status::WAITING, Status::PROCESSING], to: Status::DONE
end
首先我們看AASM的例子,它把 success: -> { update_status_time!(:done_at) } 傳進去event,因此event在這邊為 higher order function,而-> { update_status_time!(:done_at) } 為 First Class Citizens
# gem: descent_exposure
#===== 取代 controller instance variable 的好用工具
module Admin
class ReturnOrdersController < ApplicationController
expose :return_orders, -> { ReturnOrder.all }
expose :return_order, build: -> (return_order_params, scope) { scope.build(return_order_params) },
scope: -> { ReturnOrder.all }
expose :order, -> { return_order.order }
end
end
上面的例子,毫無疑問的,expose為 higher order function,而build: -> {...}, scope: -> {...} 則為被調用的First Class Citizens
自從踏入了Functional Programming的領域之後,Block變成是我很喜歡的章節之一。今天介紹了 Ruby 和 Functional Programming 之間關聯,我們也會繼續在Day8-9 介紹Block。